Repository navigation
Added support of 2-3 bit symmetric mode to LoRA-QAT - #4158
andreyanufr wants to merge 35 commits into
Conversation
2) Removed 6bit from repacking.
2) Added optional possibility to save torch checkpoint with dequantized weights.
2) Added README and requirements.txt
2) Removed debug prints.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved critical and moderate issues remain in low-bit stripping, OpenVINO repacking, and the example workflow.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 2
Open (6)
Identify zero-point constants by graph role, not shape · New Add 2/3-bit DQ packing support or reject validation · New Include gate_proj in producer search · New Handle meta-device offloaded weights during equalization · New Use logger instead of unconditional print calls · New Strengthen repacking test with graph/output validation · New
What changed in this PR
Adds INT2/INT3 symmetric LoRA-QAT support and OpenVINO weight repacking.
Changes:
- Adds 2/3-bit quantization mappings and strip validation.
- Implements and exports
repack_weights. - Adds tests, models, and a distillation QAT example with documentation.
| File | Summary and review notes |
|---|---|
tests/openvino/native/test_model_repacking.py |
Adds repacking coverage; assertions should validate graph correctness and numerical equivalence (nit, 2 votes). |
tests/openvino/native/models.py |
Adds a repacking test model. |
src/nncf/torch/function_hook/strip.py |
2/3-bit DQ still uses 4-bit decompression and packing (critical, 3 votes). |
src/nncf/quantization/quantize_model.py |
Exposes the repacking API. |
src/nncf/quantization/algorithms/weight_compression/torch_backend.py |
Adds INT2/INT3 LoRA quantization mappings. |
src/nncf/quantization/algorithms/weight_compression/openvino_backend.py |
Shape heuristic skips valid signed compressed weights, including 1-D tensors (critical, 2 votes). |
src/nncf/quantization/algorithms/weight_compression/algorithm.py |
Coordinates repacking; direct print calls should use the project logger (nit, 4 votes). |
src/nncf/quantization/__init__.py |
Exports repack_weights. |
src/nncf/openvino/quantization/quantize_model.py |
Provides the OpenVINO repacking implementation. |
src/nncf/__init__.py |
Exports the public API. |
examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit/requirements.txt |
Defines example dependencies. |
examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit/README.md |
Documents the example workflow. |
examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit/main.py |
Omits gate_proj equalization (moderate, 2 votes), skips meta-device weights (moderate, 3 votes), drops partial accumulation gradients (moderate, 1 vote), and does not return its documented metric (nit, 1 vote). |
.ci/cspell_dict.txt |
Adds example vocabulary. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if w.device.type != "meta": | ||
| return w.data | ||
| return None |
| repacked_model = nncf.repack_weights(model) | ||
|
|
||
| element_types = get_constant_element_types(repacked_model) | ||
| assert ov.Type.u3 in element_types, "Expected u3 constant after repacking" | ||
| assert ov.Type.u2 in element_types, "Expected u2 constant after repacking" |
2) Added arg for deterministic cuda execution. This arg is optional because it can slow down the training.
| ### 1. Create and activate a virtual environment | ||
|
|
||
| ```bash | ||
| cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit |
There was a problem hiding this comment.
| cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit |
Please keep same instruction steps in examples.
| ### 2. Install the example dependencies from this folder | ||
|
|
||
| ```bash | ||
| python -m pip install -r requirements.txt | ||
| ``` | ||
|
|
||
| ### 3. Install NNCF and other dependencies | ||
|
|
||
| ```bash | ||
| python3 -m pip install ../../../../ -r requirements.txt | ||
| ``` |
There was a problem hiding this comment.
| ### 2. Install the example dependencies from this folder | |
| ```bash | |
| python -m pip install -r requirements.txt | |
| ``` | |
| ### 3. Install NNCF and other dependencies | |
| ```bash | |
| python3 -m pip install ../../../../ -r requirements.txt | |
| ``` | |
| ### 2. Install NNCF and other dependencies | |
| ```bash | |
| python3 -m pip install ../../../../ -r requirements.txt |
?
There was a problem hiding this comment.
What about local requirements like datasets ? We do not have them in ../../../../ -r requirements.txt
There was a problem hiding this comment.
../../../../ - installing NNCF with pakage's requarements
-r requirements.txt - installing example's requarements
| cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit | ||
| python main.py --pretrained meta-llama/Llama-3.2-1B-Instruct --bits 3 --dataset pile-10k --epochs 1 --output_dir output |
There was a problem hiding this comment.
| cd examples/llm_compression/torch/distillation_qat_with_lora_2_3_bit | |
| python main.py --pretrained meta-llama/Llama-3.2-1B-Instruct --bits 3 --dataset pile-10k --epochs 1 --output_dir output | |
| python main.py |
For example, just to run main.py with default arguemts
| - `--scale_epochs`: extra epochs where only quantizer scales are trained | ||
| - `--linear_lr_scheduler`: use a linear learning-rate decay schedule | ||
| - `--batch_size`: accumulation target | ||
| - `--microbatch_size`: per-step microbatch size |
There was a problem hiding this comment.
| - `--microbatch_size`: per-step microbatch size | |
| - `--microbatch_size`: per-step microbatch size | |
| - `--full_determinism`: to enable deterministic QAT |
| - OpenVINO export is always performed at the end of the run, after checkpoint restoration and stripping. | ||
| - `--resume` reuses the checkpoint if present; otherwise the script initializes from scratch. | ||
|
|
||
| For more background on absorbable LoRA and low-bit training-time compression, see the project documentation for QAT LoRA usage and NNCF compression flows. |
There was a problem hiding this comment.
I'm not sure if this is necessary,
or add links
| if hook_module._qspec.half_range or hook_module._qspec.narrow_range: | ||
| msg += "Unexpected parameters of quantizers on strip: half_range and narrow_range should be False.\n" | ||
| if hook_module.num_bits not in [4, 8]: | ||
| if hook_module.num_bits not in [2, 3, 4, 8]: |
There was a problem hiding this comment.
Looks this function should work for any num_bits, and this condition can be removed?
| supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8} | ||
| if ov_node.tensor_view.element_type not in supported_src_types: | ||
| return False, 0, 0 | ||
| bits = supported_src_types[ov_node.tensor_view.element_type] | ||
| asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4] | ||
|
|
||
| # only repack for sym types are supported for now: i3, i2 | ||
| if asym: | ||
| return False, 0, 0 |
There was a problem hiding this comment.
| supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8} | |
| if ov_node.tensor_view.element_type not in supported_src_types: | |
| return False, 0, 0 | |
| bits = supported_src_types[ov_node.tensor_view.element_type] | |
| asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4] | |
| # only repack for sym types are supported for now: i3, i2 | |
| if asym: | |
| return False, 0, 0 | |
| supported_src_types = {ov.Type.i4: 4, ov.Type.i8: 8} | |
| if ov_node.tensor_view.element_type not in supported_src_types: | |
| return False, 0, 0 |
?
| bits = supported_src_types[ov_node.tensor_view.element_type] | ||
| asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4] |
There was a problem hiding this comment.
| bits = supported_src_types[ov_node.tensor_view.element_type] | |
| asym = ov_node.tensor_view.element_type in [ov.Type.u8, ov.Type.u4] | |
| bits = ov_node.tensor_view.element_type.get_bitwidth() | |
| asym = ov_node.tensor_view.element_type.is_signed() |
|
|
||
| ov_node = self.name_to_node_mapping[node.node_name] | ||
|
|
||
| supported_src_types = {ov.Type.u4: 4, ov.Type.i4: 4, ov.Type.u8: 8, ov.Type.i8: 8} |
There was a problem hiding this comment.
Looks like now suppoerted only i4 and i8
| if hasattr(weight_compression_parameters, "sequence"): | ||
| # avoid track | ||
| is_all_8bit = all( | ||
| wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters.sequence | ||
| ) | ||
| else: | ||
| is_all_8bit = all(wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters) |
There was a problem hiding this comment.
| if hasattr(weight_compression_parameters, "sequence"): | |
| # avoid track | |
| is_all_8bit = all( | |
| wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters.sequence | |
| ) | |
| else: | |
| is_all_8bit = all(wc_params.compression_config.num_bits == 8 for wc_params in weight_compression_parameters) | |
| is_all_8bit = all( | |
| wc_params.compression_config.num_bits == 8 | |
| for wc_params in getattr(weight_compression_parameters, "sequence", weight_compression_parameters) | |
| ) |
Looks like track uses alwasy here but ok
| @@ -0,0 +1,879 @@ | |||
| # Copyright (c) 2026 Intel Corporation | |||
There was a problem hiding this comment.
Maybe we can rename the example to QAD with Lora since it seems more suitable
| transformers.enable_full_determinism(42) | ||
| else: | ||
| transformers.set_seed(42) |
There was a problem hiding this comment.
do we need the else statemnet? does transformers.set_seed(42) affect full_determinism()?
There was a problem hiding this comment.
This parameter can slow down training, so I suggest keeping it optional.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical repacking and Torch backend issues, along with additional correctness and validation gaps, remain unresolved.
Review effort: Lite
Findings: 2
Open (6)
Select signed bit width based on the actual offset range · New Reject or implement INT2/INT3 support for Torch DQ · New Validate microbatch accumulation and flush partial batches · New Preserve the original Convert element type when replacing it · New Handle meta-device offloaded weights during equalization Strengthen repacking test with graph/output validation
| max_val = max(int(weight_tensor.max().item()), abs(int(weight_tensor.min().item()))) | ||
| optimal_bits = int(max_val).bit_length() | ||
|
|
||
| supported_optimal_bits = {2: ov.Type.u2, 3: ov.Type.u3} | ||
|
|
||
| # If the optimal bits is equal to the current bits, no need to repack | ||
| if optimal_bits == bits or optimal_bits not in supported_optimal_bits: |
| CompressWeightsMode.INT2_SYM: QuantizationScheme.SYMMETRIC_LORA, | ||
| CompressWeightsMode.INT3_SYM: QuantizationScheme.SYMMETRIC_LORA, |
| opt = torch.optim.AdamW(param_to_train, weight_decay=weight_decay) | ||
|
|
||
| # Run tuning with distillation loss and validation after each epoch. | ||
| grad_accumulation_steps = args.batch_size // args.microbatch_size |
| converted_const = opset.convert(compressed_const, ov.Type.f16) | ||
|
|
||
| zero_point_const = opset.constant(offset, dtype=ov.Type.i8, name=f"{const_node_name}/zero_point") | ||
| zero_point_const = opset.convert(zero_point_const, ov.Type.f16) | ||
|
|
||
| converted_const = opset.subtract( | ||
| converted_const, zero_point_const, name=f"{const_node_name}/zero_point/subtract" | ||
| ) |
2) Fixed convert type after repacking. 3) Added tokenizer to export.
|
|
||
| This is a real training pipeline for low-bit LLM optimization, not a simple inference example. | ||
|
|
||
| ## Prerequisites |
There was a problem hiding this comment.
Is there any minimal or recommended HW? If so, let's mention it.
|
|
||
| ### Model and compression | ||
|
|
||
| - `--pretrained`: HF model ID or local path. |
There was a problem hiding this comment.
I don't think it works with any model. Can we add a note somewhere which models this example was tested with?
| @@ -0,0 +1,123 @@ | |||
| # Quantization Aware Distillation with absorbable LoRA for 2/3-bit LLM compression | |||
There was a problem hiding this comment.
Let's try to explain all abbreviations when they're mentioned first time



Changes
LoRA-QAT now support two new modes: CompressWeightsMode.INT3_SYM and CompressWeightsMode.INT2_SYM.
New function repack_weights was added to repack 4-8 bit weights to lower bit representation if possible.
Reason for changes
Support of 2-3 bit in compression in QAT and pytorch backend.
Related tickets
CVS-180191
Tests
In progress.
Weight compression - success